/**
 * @file form
 * @author liaoxuezhi@baidu.com
 */
/* eslint fecs-indent: [0, "space", 2, 2] */

import * as React from 'react';
import {
  Component,
  PureComponent,
  PropTypes
} from 'react';
import {findDOMNode} from 'react-dom';
import {Renderer, render, evalExpression} from '../../renderer';
import * as cx from 'classnames';
import * as assign from 'object-assign';
import { DragSource, DropTarget } from 'react-dnd';
import {container} from '../../propTypes';
import {Enhance as HocCrud} from '../hoc-crud';
import {resolveVariable} from '../../util';
import {isBreakpoint} from '../../../util/helper';

import './heading';
import './field';

import './table.scss';

const returnFalse = () => false;

const CARD_TYPE = 'row';
@Renderer({
  name: 'table',
  desc: '通用的table模型',
  test: /(?:^|\/)crud\/table$/
})
@HocCrud()
export default class Table extends PureComponent {
  static propTypes = {
    bulkActions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string,
      actionType: PropTypes.string.isRequired
    })),
    defaultChecked: PropTypes.bool,
    className: PropTypes.string,
    tableWrapClassName: PropTypes.string,
    tableClassName: PropTypes.string,
    draggable: PropTypes.bool,
    columns: PropTypes.arrayOf(PropTypes.object),
    data: PropTypes.object,
    rows: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.string])),
    // data: PropTypes.arrayOf(PropTypes.shape({
    //   id: PropTypes.oneOfType([
    //     PropTypes.number,
    //     PropTypes.string
    //   ]).isRequired
    // })),
    toolbar: container,
    title: PropTypes.string,
    loading: PropTypes.bool,


    columnsTogglable: PropTypes.bool,
    // events
    onOrderByToggle: PropTypes.func,
    onPerPageChange: PropTypes.func,
    onPaginationChange: PropTypes.func,
    onBulkAction: PropTypes.func,
    onAction: PropTypes.func,
    onSaveOrder: PropTypes.func,
    onQuickSave: PropTypes.func,
    onNotify: PropTypes.func
  };

  static defaultProps = {
    title: '表格',
    className: 'panel-default',
    toolbarClassName: 'padder m-t m-b',
    tableWrapClassName: 'table-responsive',
    tableClassName: 'table-striped',
    placeholder: '没有数据',
    primaryField: 'id',

    switchPerPage: false, // 是否能切换页码
    showPageInfo: true, // 是否显示数据信息
    defaultChecked: false, // 默认是否选中

    columnsTogglable: true,
    checkOnRowClick: false
  };

  static contextTypes = {
    can: PropTypes.func
  };

  constructor(props) {
    super(props);
    
    const columns = this.buildColumns(props);
    this.state = {
      columns,
      rows: this.buildRows(props, columns),
      footableExpandAll: props.footable && props.footable.expand === 'all',
      hiddenCols: props.columnsTogglable && !props.footable ? this.buildHiddenColsIndex(columns) : {},
      columnsTogglable: props.columnsTogglable && !props.footable
    };

    this.setColumnsTogglableCol = this.setColumnsTogglableCol.bind(this);
    this.handleResize = this.handleResize.bind(this);
    this.bindResize = this.bindResize.bind(this);
    this.toggleAllFootable = this.toggleAllFootable.bind(this);
  }

  componentDidMount() {
    this.setColumnsTogglableCol();
    this.bindResize();
  }

  componentWillReceiveProps(nextProps) {
    const props = this.props;

    if(props.stateTriggerBuild != nextProps.stateTriggerBuild || props.columns !== nextProps.columns || props.data !== nextProps.data) {
      let columns = this.buildColumns(nextProps);
      this.setState({columns, rows: this.buildRows(nextProps, columns)});
    } else if (props.stateRows !== nextProps.stateRows) {
      this.setState({
        rows: this.buildRows(nextProps, this.state.columns)
      });
    }

    if(props.stateColumns !== nextProps.stateColumns) {
      let hiddenCols = this.buildHiddenColsIndex(nextProps.stateColumns);
      let columns = this.buildColumns(nextProps);
      this.setState({hiddenCols, columns, rows: this.buildRows(nextProps, columns)}, this.setColumnsTogglableCol);
    }
  }

  componentDidUpdate() {
    this.bindResize();
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize);
  }

  handleResize() {
    this.setState({
      columns: this.buildColumns(this.props)
    });
  }

  bindResize() {
    window.removeEventListener('resize', this.handleResize);

    if (this.props.footable) {
      window.addEventListener('resize', this.handleResize);
    }
  }


  buildHiddenColsIndex(columns) {
    let obj = {};
    columns && columns.forEach((item, index) => {
      item.toggled === false && (obj[index] = true);
    });
    return obj;
  }

  setColumnsTogglableCol(){
    const {columns, hiddenCols} = this.state;
    const {setColumnsTogglableCol} = this.props;

    let newCol = columns.map((value, index) => hiddenCols[index] === true ? assign(value, {toggled: false}) : assign(value, {toggled: true}) );

    setColumnsTogglableCol && setColumnsTogglableCol(newCol);
  }

  buildColumns(props) {
    let columns = props.columns ? props.columns.concat() : [];
    const data = assign({}, props.data, {acl: {can: this.context && this.context.can || returnFalse}});

    const bulkActions = props.bulkActions;
    const draggable = props.draggable;
    const dragging = props.stateDragging;

    if (draggable && dragging) {
      columns.unshift({
        type: 'drag-bar',
        width: 20
      });

      columns = columns.filter(item => item.type !== 'operation');
    } else if (bulkActions && bulkActions.length) {
      columns.unshift({
        type: 'checkbox',
        width: 20
      });
    }

    columns = columns.filter(item => {
      if (!item) {
        return false;
      }

      return item.visibleOn
        ? evalExpression(item.visibleOn, data) : item.hiddenOn
          ? !evalExpression(item.hiddenOn, data) : true;
    });

    if (props.footable) {
      columns.map(item => {
        item.$footable = item.breakpoint === true
          || typeof item.breakpoint === 'string' && isBreakpoint(item.breakpoint);
        return item;
      });
    }

    return columns;
  }

  buildRows(props, columns) {
    let rows = props.stateRows;

    if (props.footable && columns.some(item => item.$footable)) {
      let expand = props.footable.expand || 'first';
      rows = rows.map((item, idx) => assign({
        $footable: expand === 'all' || expand === 'first' && idx === 0
      }, item));
    }

    return rows;
  }

  toggleFootable(index, stopClickPropagation, e) {
    if (stopClickPropagation) {
      e.stopPropagation();
    }
    const rows = this.state.rows.concat();
    const item = rows[index];
    item && rows.splice(index, 1, assign({}, item, {
      $footable: !item.$footable
    }));

    this.setState({
      rows
    });
  }

  toggleAllFootable() {
    const prevExpanded = this.state.footableExpandAll;
    let rows = this.state.rows.map(item => assign({}, item, {
      $footable: !prevExpanded
    }));

    this.setState({
      footableExpandAll: !prevExpanded,
      rows
    })
  }

  renderContainer(children, defaultProps = {}) {
    const {
      $path,
      sendNotify,
      locals,
      data,
      onRequestAction
    } = this.props;

    if (!Array.isArray(children)) {
      children = [children];
    }

    return children.map((child, key) => render(`${$path}/${child.type}`, assign({}, defaultProps, child), {
      key,
      sendNotify,
      locals,
      data,
      onRequestAction
    }));
  }

  buildLabelGroup(columns) {
    const group = [];
    let haveLabelGroup = false;
    columns.reduce((lastCol, col) => {
      let item;
      col.labelGroup && (haveLabelGroup = true);
      if(lastCol && col.labelGroup === lastCol.label) {
        lastCol.length++;
        item = lastCol;
      } else {
        item = {
          label: col.labelGroup,
          length: 1
        };
        group.push(item);
      }
      return item;
    }, null);

    return haveLabelGroup ? group.map((item,index) => <th className="text-center" colSpan={item.length} key={index}>{item.label}</th>) : null;
  }

  renderHeading() {
    const {
      hiddenCols,
      columnsTogglable
    } = this.state;

    let columns = this.state.columns;
    const rows = this.props.stateRows || [];
    const {
      $path,
      orderBy,
      orderDir,
      onOrderByToggle,
      loading,
      stateLocalOrderBy,
      stateLocalOrderDir,
      localSortByName,
      footable,
      headerAffix
    } = this.props;

    const $allChecked = rows.length ? rows.every(item => item.$checked) : false;

    if (columnsTogglable && !footable) {
      columns = columns.filter((item, index) => hiddenCols[index] !== true);
    }

    const labelGroup = this.buildLabelGroup(columns);
    let prefixAttached = false;
    const footableColumns = [];

    if (footable) {
      columns = columns.filter(item => {
        if (item.$footable) {
          footableColumns.push(item);
          return false;
        }
        return true;
      })
    }

    return (
      <thead>
        {
          labelGroup ? <tr>{labelGroup}</tr> : null
        }
        <tr>
          {columns
            .map((field, key) => {
              let prefix;

              if (!field.type) {
                field.type = 'text';
              }

              if (!prefixAttached && footableColumns.length && field.type !== 'checkbox') {
                prefixAttached = true;
                prefix = (
                  <span
                    key="footable-toggle"
                    className="btn btn-xs btn-link btn-footable"
                    onClick={this.toggleAllFootable}
                  >
                    <i className={this.state.footableExpandAll ? 'fa fa-minus' : 'fa fa-plus'} />
                  </span>
                );
              }

              return render(`${$path}/heading`, assign({type: 'text'}, field), {
                key,
                orderBy,
                orderDir,
                onOrderByToggle: onOrderByToggle,
                dragging: this.props.stateDragging,
                $allChecked,
                onCheck: headerAffix ? null : this.props.toggleAll,
                onSearch: this.props.onSearch,
                onInit: this.props.onInit,
                params: this.props.params,
                loading,
                disableSearch: loading || this.props.stateDragging,
                localSortByName,
                stateLocalOrderBy,
                stateLocalOrderDir,
                prefix
              });
            })}
        </tr>
      </thead>
    )
  }

  renderBody() {
    const {
      hiddenCols,
      columnsTogglable
    } = this.state;
    let columns = this.state.columns;
    const rows = this.state.rows || [];
    const dragging = this.props.stateDragging;
    const {
      $path,
      draggable,
      onAction,
      onHighlight,
      loading,
      placeholder,
      data,
      stateDragging,
      onNotify,
      locals,
      footable,
      checkOnRowClick
    } = this.props;

    const length = rows.length;

    if (columnsTogglable && !stateDragging && !footable) {
      columns = columns.filter((item, index) => hiddenCols[index] !== true);
    }

    const doms = [];
    const footableColumns = [];

    if (footable) {
      columns = columns.filter(item => {
        if (item.$footable) {
          footableColumns.push(item);
          return false;
        }
        return true;
      })
    }

    if (length) {
      rows.forEach((item, idx) => {
        let prefixAttached = false;
        const body = columns.map((field, key) => {

          let prefix;

          if (!field.type) {
            field.type = 'text';
          }

          if (!prefixAttached && footableColumns.length && field.type !== 'checkbox') {
            prefixAttached = true;
            prefix = (
              <span
                key="footable-toggle"
                className="btn btn-xs btn-link btn-footable"
                onClick={this.toggleFootable.bind(this, idx, checkOnRowClick)}
              >
                <i className={item.$footable ? 'fa fa-minus' : 'fa fa-plus'} />
              </span>
            );
          }

          return render(`${$path}/field`, field, {
            key,
            item: item,
            data: data,
            onCheck: this.props.toggle,
            onRequestAction: onAction,
            onRequestHighlight: onHighlight,
            onFieldValueChange: this.props.handleFieldValueChange,
            onFieldValueSave: this.props.handleFieldValueSave,
            onNotify: onNotify,
            value: resolveVariable(field.name, item),
            disabled: loading,
            disableQuickEdit: dragging || loading,
            locals: locals,
            prefix,
            stopClickPropagation: checkOnRowClick
        });
        });

        if (draggable && dragging && !item.$footable) {
          return doms.push(
            <DraggableTr
              key={item.$id}
              id={item.$id}
              findRow={this.props.findRow}
              moveRow={this.props.moveRow}
              moveRowConfirm={this.props.moveRowConfirm}
              className={cx({
                'tr-modified': item.$modified,
                'tr-highlight': item.$highlight
              })}
            >
              {body}
            </DraggableTr>
          )
        }

        doms.push(
          <tr
            key={`row-${item.$id}`}
            className={cx({
              'tr-modified': item.$modified,
              'tr-highlight': item.$highlight,
              'tr-checked': item.$checked,
              'tr-odd': idx % 2 === 0,
              'tr-even': idx % 2 === 1
            })}
            onClick={checkOnRowClick ? e => e.target.tagName === 'INPUT' || this.props.toggle(!item.$checked, item) : null}
          >
            {body}
          </tr>
        );

        if (item.$footable && footableColumns.length) {
          doms.push(
            <tr
              key={`${item.$id}-foot`}
              className={cx({
                'tr-modified': item.$modified,
                'tr-highlight': item.$highlight,
                'tr-checked': item.$checked,
                'tr-odd': idx % 2 === 0,
                'tr-even': idx % 2 === 1
              })}
              onClick={checkOnRowClick ? e => e.target.tagName === 'INPUT' || this.props.toggle(!item.$checked, item) : null}
            >
              <td colSpan={columns.length}>
                <table className="footable-details table no-bg">
                  <tbody>
                    {footableColumns.map((field, key) => field.label ? (
                      <tr
                        key={key}
                      >
                        <th>{field.label}</th>
                        {render(`${$path}/field`, field, {
                          key,
                          item: item,
                          data: data,
                          onCheck: this.props.toggle,
                          onRequestAction: onAction,
                          onFieldValueChange: this.props.handleFieldValueChange,
                          onFieldValueSave: this.props.handleFieldValueSave,
                          onNotify: onNotify,
                          value: resolveVariable(field.name, item),
                          disabled: loading,
                          disableQuickEdit: dragging || loading,
                          locals: locals,
                          stopClickPropagation: checkOnRowClick
                        })}
                      </tr>
                    ) : (
                        <tr
                          key={key}
                        >
                          {render(`${$path}/field`, field, {
                            key,
                            item: item,
                            data: data,
                            onCheck: this.props.toggle,
                            onRequestAction: onAction,
                            onFieldValueChange: this.props.handleFieldValueChange,
                            onFieldValueSave: this.props.handleFieldValueSave,
                            onNotify: onNotify,
                            value: resolveVariable(field.name, item),
                            disabled: loading,
                            disableQuickEdit: dragging || loading,
                            locals: locals,
                            colSpan: 2,
                            stopClickPropagation: checkOnRowClick
                          })}
                        </tr>
                      ))}
                  </tbody>
                </table>
              </td>
            </tr>
          )
        }
      })
    } else {
      doms.push(<tr key="sum"><td colSpan={columns.length}>{placeholder}</td></tr>);
    }

    return (
      <tbody>
        {doms}
      </tbody>
    );
  }

  render() {
    const {
      tableClassName,
      tableWrapClassName,
      footable
    } = this.props;

    return (
        <div className={tableWrapClassName}>
          <table
            className={cx('table table-db b-light', tableClassName, {
              footable
            })}
          >
            {this.renderHeading()}
            {this.renderBody()}
          </table>
        </div>
    );
  }
}

/**
 * 让 tr 可拖拽
 */
const cardTarget = {
  canDrop() {
    return true;
  },

  hover(props, monitor, component) {
    const { id: draggedId, originalIndex: dragIndex} = monitor.getItem();
    const { id: overId } = props;

    if (draggedId !== overId) {
      const { index: overIndex } = props.findRow(overId);

      const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;

      // Dragging downwards
      if (dragIndex < overIndex && hoverClientY < hoverMiddleY) {
        return;
      }

      // Dragging upwards
      if (dragIndex > overIndex && hoverClientY > hoverMiddleY) {
        return;
      }

      props.moveRow(draggedId, overIndex);
    }
  }
};
const cardSource = {
  beginDrag(props) {
    return {
      id: props.id,
      originalIndex: props.findRow(props.id).index
    };
  },

  endDrag(props, monitor) {
    const { id: droppedId, originalIndex } = monitor.getItem();
    const didDrop = monitor.didDrop();

    if (didDrop) {
      props.moveRowConfirm(droppedId);
    } else {
      props.moveRow(droppedId, originalIndex);
    }
  }
};
@DropTarget(CARD_TYPE, cardTarget, connect => ({
  connectDropTarget: connect.dropTarget()
}))
@DragSource(CARD_TYPE, cardSource, (connect, monitor) => ({
  connectDragSource: connect.dragSource(),
  connectDragPreview: connect.dragPreview(),
  isDragging: monitor.isDragging()
}))
class DraggableTr extends PureComponent {
  static propTypes = {
    connectDragSource: PropTypes.func.isRequired,
    connectDragPreview: PropTypes.func.isRequired,
    connectDropTarget: PropTypes.func.isRequired,
    id: PropTypes.oneOfType([
      PropTypes.number,
      PropTypes.string
    ]).isRequired,
    isDragging: PropTypes.bool.isRequired,
    findRow: PropTypes.func.isRequired,
    moveRow: PropTypes.func.isRequired,
    moveRowConfirm: PropTypes.func.isRequired
  };

  render() {
    const {
      isDragging,
      connectDragSource,
      connectDragPreview,
      connectDropTarget,
      className
    } = this.props;

    // console.log('Re render');

    return connectDropTarget(connectDragPreview(
      <tr
        className={cx(className, {
          'tr-dragging': isDragging
        })}
      >
        {React.Children.map(this.props.children, child => {
          if (child && child.props.type === 'drag-bar') {
            child = React.cloneElement(child, assign({
              ref: (component) => {
                const dom = findDOMNode(component);
                connectDragSource(dom);
              }
            }, child.props));
          }

          return child;
        })}
      </tr>
    ));
  }
}